<?php
/**
 *  Generic Abstract Model: per-row table access
 *
 *  Similar to Active Record pattern, in which the record is implemented in Item,
 *      and the data access in Action.
 *  Implement the common methods of selecting items and executing actions
 *
 */
namespace Yz\modules\Generic;

abstract class Model_Abstract
    extends \Yz_Model_Abstract
    implements \Yz_Model_Interface
{

    const DEFAULT_ITEM = "Default";

    const SOURCE_IN_CONFIG = true;
    const SOURCE_DEFAULT = '~';
    /**
     * a string which may contain SOURCE_DEFAULT
     * or the value of SOURCE_IN_CONFIG which tells to use config
     */
    const SOURCE_DEFAULT_TABLE = self::SOURCE_IN_CONFIG;
    const SOURCE_DEFAULT_ALIAS = 't0';

    /**
     *  @var string|null
     *  NOTE caution: if this is a static config string:
     *      ALL entries of self::SOURCE_DEFAULT will be replaced by the default source name
     */
    protected static $sourceConfig = self::SOURCE_IN_CONFIG;

    /**
     *  @var Yz_Hash
     */
    protected $sourceDescriptor;

    /**
     *  a name of item class (Yz_Item_Interface)
     *  @var string
     */
    protected $itemName;

    /** the list of loaded item template objects
     *  @var array
     */
    protected $items = array();



    /************************************
     *          Public methods          *
     ************************************/


    /**
     *  get item class
     *  @return string
     */
    public function fetchItemConfig(\Yz_Item_Interface $item)
    {
        debug();
        return $this->module
            ->getConfig()
            ->fetchSectionWithInherit('Item.'.$item->shortClassName())
            ;
    }


    /**
     *  create item template
     *  @return Yz_Item_Interface
     */
    public function selectItem($name=null)
    {
        debug($name);
        if (null === $name) {
            $name = $this->itemName;
            if (null === $name) {
                $name = static::DEFAULT_ITEM;
            }
        } else {
            $name = ucfirst(\Yz_String::testNotEmpty($name));
        }

        if (array_key_exists($name, $this->items)) {
            $itemClass = $this->items[$name];
        } else {
            $itemClass = $this->identifyItemClass($name);
            $this->items[$name] = $itemClass;
        }
        $this->itemName = $name;
        return new $itemClass($this);
    }



    /************************************
     *       Protected methods          *
     ************************************/



    /**
     *  identify item class
     *  @return string
     */
    protected function identifyItemClass($name)
    {
        debug();
        $class = $this->module->resolveLibrary('Item', $name);
        assert('is_subclass_of($class,"Yz_Item_Interface")');
        return $class;
    }


    /**
     *  create action
     *  @return Yz_Action_Interface
     */
    protected function selectAction( $actionName )
    {
        debug();
        $class = $this->module->resolveLibrary('Action', $actionName);
        assert('is_subclass_of($class,"Yz_Action_Interface")');

        // TODO take class name from property of $this
        $action = new $class($this->describeSource());

        return $action;
    }


    /**
     *  get a hash with item's source names (SQL table aliases) as keys and SQL table names as values
     *
     *  The default implementation returns one source entry
     *  with the table name built from the current module's namespace
     *  and with the alias 't0'
     *  @return Yz_Hash
     *  @todo use config 'sources'
     */
    protected function getSourceFromConfig()
    {
        debug();
        $out = array();
        $defaultSource = $this->getDefaultSource();
        $s = $this->getConfig()->fetchSection("source");
        if (null === $s) {
            $out[static::SOURCE_DEFAULT_ALIAS] = $defaultSource;
#            throw new \Yz_Exception("Failed to identify sources");
        } else {
            foreach ($s as $alias => $data) {
                if (is_string($data)) {
                    $src = str_replace(static::SOURCE_DEFAULT, $defaultSource, $data);
                } else {
                    $src = $data;
                    if ($src->offsetExists('name')) {
                        $src['name'] = str_replace(static::SOURCE_DEFAULT, $defaultSource, $src['name']);
                    } else {
                        // ERROR, name must be present
                    }
                }
                $out[$alias] = $src;
            }
        }
        return new \Yz_Hash($out);
    }


    /** use SOURCE_DEFAULT_TABLE as default table
     *  + parse static::$sourceConfig as Yz_Config
     *  @return Yz_Config
     */
    protected function getSourceFromStaticConfig()
    {
        debug();
        $c = new \Yz_Config();

        // substitute default source
        $d = static::SOURCE_DEFAULT;
        $defaultSource = $this->getDefaultSource();

        // handle default table if it is usable
        $t = static::SOURCE_DEFAULT_TABLE;
        if ("" !== "$t") {
            $c[static::SOURCE_DEFAULT_ALIAS] = str_replace($d, $defaultSource, $t);
            #$t = str_replace(static::SOURCE_DEFAULT, $defaultSource, $t);
        }

        // handle source configs using inheritance
        $configs = array();
        $class = $this->className(); // must use a string below
        $prev = null;
        while (is_string($class::$sourceConfig) ) {
            // the loop will guaranteedly terminate on abstract model
            // in which the $sourceConfig exists and is null

            $ent = $class::$sourceConfig;
            "" !== $ent
                and $prev !== $ent
                and $prev = $ent
                and $configs[$class] = $ent;
            $class = get_parent_class($class);
        }


        if ($configs) {
            foreach (array_reverse($configs, true) as $class => $ent) {
                // substitute default source
                $ent = str_replace($d, $defaultSource, $ent);
                $c = \Yz_Config_Loader::load($ent, "{$class}.sourceConfig", $c);
            }
        }

        if ($c->isEmpty()) {
            throw new \Yz_Exception("Failed to determine model's source configuration");
        }

        return $c;
    }


    /**
     *
     *  @return string - lowercase module-name
     */
    protected function getDefaultSource()
    {
        debug();
        return strtolower($this->module->getFullName());
    }

    protected function describeSource()
    {
        $s =& $this->sourceDescriptor;
        if (null === $s) {
            if (self::SOURCE_IN_CONFIG === static::$sourceConfig) {
                $s = $this->getSourceFromConfig();
            } else {
                $s = $this->getSourceFromStaticConfig();
            }
        }
        return $s;
    }



    /** Execute a fetch action and return item complete with values
     *  @param action
     *  @return item
     *  @throw ItemNotFound
     */
    protected function _fetch(\Yz_Action_Interface $action)
    {
        debug();
        $item = $this->selectItem();
        $fields = $item->describe();
        $values = $action
            ->assignFields($fields)
            ->perform()
            ->fetch()
        ;
        #return $item->assignValues($values);
        return $this->assignValues($values, $item);
    }


    /** Execute a fetch action and return item complete with values
     *  @param action
     *  @return item
     *  @throw ItemNotFound
     */
    protected function _update(\Yz_Item_Interface $item, \Yz_Action_Interface $action)
    {
        debug();
        $fields = $item->describe();
        $action
            ->assignFields($fields)
            ->assignValues($item)
            ->perform()
        ;
        return $this;
    }


    /** assign values to an item
     *  @param Yz_Hash values
     *  @param item
     *  @return item
     */
    protected function assignValues($values, $item)
    {
        return $item->assignValues($values);
    }


}
